package ee.parameterConverter.impl;

import java.lang.reflect.Array;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import ee.parameterConverter.ConversionFailedException;
import ee.parameterConverter.Converter;
import ee.parameterConverter.ConverterResolver;
import ee.parameterConverter.EmptyValueException;
import ee.parameterConverter.ListConverter;
import ee.parameterConverter.ValidationResultCollector;

public class ListConverterImpl implements ListConverter {

	private ConverterResolver _converterResolver;
	private ParameterConverterImpl _parameterConverter;
	private ValidationResultCollector _validationResultCollector;

	public ListConverterImpl(ParameterConverterImpl parameterConverter, ConverterResolver converterResolver, ValidationResultCollector validationResultCollector)
	{
		_parameterConverter = parameterConverter;
		_converterResolver = converterResolver;
		_validationResultCollector = validationResultCollector;
	}
	
	@Override
	public Object convert(String[] values, Type type) throws ConversionFailedException, EmptyValueException {
		
		// see if we have a converter
		Converter<String[], ?> converter = _converterResolver.getStringArrayConverter(type);

		if (converter != null) {
			return converter.convert(values);
		}
		
		ListTypeInfo listTypeInfo = _getListTypeInfo(type, values.length);
		
		Type valueType = listTypeInfo.valueType;
		
		Wrapper wrapper = listTypeInfo.wrapper;
		
		for (String value : values)
		{
			try
			{
				wrapper.add(_parameterConverter.convert(value, valueType));
			} catch (EmptyValueException e)
			{
				//dont add the empty value
			}
		}
		
		if (wrapper.size() == 0) {
			throw new EmptyValueException("The resulting list is empty");
		}
		
		return wrapper.getResult();
	}

	@Override
	public Object convert(Map<String, Object> normalizedParameters, Type type,
			List<String> parameterPath) throws ConversionFailedException, EmptyValueException {
		Set<String> keys = normalizedParameters.keySet();
		ListTypeInfo listTypeInfo = _getListTypeInfo(type, keys.size());
		Type valueType = listTypeInfo.valueType;
		Wrapper wrapper = listTypeInfo.wrapper;
		
		for (String key : keys)
		{
			Object value = normalizedParameters.get(key);
			
			int position;
			try
			{
				position = Integer.parseInt(key);
			} catch (NumberFormatException e)
			{
				throw new ConversionFailedException("Could not convert to list because the keys of the given parameters are not numeric", value);
			}
			
			ArrayList<String> currentParameterPath = new ArrayList<String>(parameterPath);
			currentParameterPath.add(key);
			
			try
			{
				if (value instanceof String[])
				{
					wrapper.add(position, _parameterConverter.convert((String[]) value, valueType));
				} else
				{
					@SuppressWarnings("unchecked")
					Map<String, Object> valueMap = (Map<String, Object>) value;
					wrapper.add(position, _parameterConverter.convert(valueMap, valueType, currentParameterPath));
				}
				
					
			} catch (EmptyValueException e) {
				//the value is empty, don't add it
			} catch (Throwable e)
			{
				_validationResultCollector.processException(currentParameterPath, e);
			}
		}
		
		if (wrapper.size() == 0) {
			throw new EmptyValueException("The resulting list is empty");
		}
		
		return wrapper.getResult();
	}

	private ListTypeInfo _getListTypeInfo(Type type, int valueCount) throws ConversionFailedException {
		ListTypeInfo listTypeInfo;
		
		if (type instanceof ParameterizedType)
		{
			ParameterizedType parameterizedType = (ParameterizedType) type;
			Class<?> rawType = (Class<?>) parameterizedType.getRawType();
			if (List.class.isAssignableFrom(rawType))
			{
				listTypeInfo = new ListTypeInfo(parameterizedType.getActualTypeArguments()[0], new ListWrapper(valueCount));
			} else
			{
				throw new ConversionFailedException(
				"Could not convert to a List type because target type does not implement List");
			}
		}
		else if (type instanceof Class)
		{
			Class<?> classType = (Class<?>) type;
			if (classType.isArray())
			{
				Class<?> componentType = classType.getComponentType();
				listTypeInfo = new ListTypeInfo(componentType, new ArrayWrapper(componentType, valueCount));
			} else
			{
				throw new ConversionFailedException(
				"Could not convert to a List type because no generic information is present. The given type should be an instance of ParameterizedType or an array");
			}
		} else
		{
			throw new ConversionFailedException(
					"Could not convert to a List type, unknown type: " + type);
		}
		
		return listTypeInfo;
	}	
	
	private class ListTypeInfo
	{
		Type valueType;
		Wrapper wrapper;
		
		ListTypeInfo(Type valueType, Wrapper wrapper)
		{
			this.valueType = valueType;
			this.wrapper = wrapper;
		}
	}
	
	private interface Wrapper
	{
		void add(Object value);
		void add(int position, Object value);
		Object getResult();
		int size();
	}
	
	private class ArrayWrapper implements Wrapper
	{
		private Object _array;
		private int _position;
		private int _size;
		
		ArrayWrapper(Class<?> componentType, int size)
		{
			_array = Array.newInstance(componentType, size);
			_position = 0;
			_size = size;
		}

		@Override
		public void add(Object value) {
			Array.set(_array, _position++, value);
		}
		
		@Override
		public void add(int position, Object value) {
			
			Array.set(_array, _position, value);
		}

		@Override
		public Object getResult() {
			return _array;
		}

		@Override
		public int size() {
			return _size;
		}
	}
	
	private class ListWrapper implements Wrapper
	{
		private List<Object> _list;
		private int _size;
		
		ListWrapper(int size)
		{
			_list = new ArrayList<Object>(size);
			_size = size;
		}
		
		@Override
		public void add(Object value) {
			_list.add(value);
		}

		@Override
		public void add(int position, Object value) {
			int size = _list.size();
			
			if (position >= size)
			{
				_list.addAll(Collections.nCopies((position - size) + 1, null));
			}
			
			_list.set(position, value);
		}

		@Override
		public Object getResult() {
			return _list;
		}

		@Override
		public int size() {
			return _size;
		}
		
	}
	
}
